package bancosys.tec.persist.hibernate.page;

import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;

import org.hibernate.Criteria;
import org.hibernate.criterion.CriteriaSpecification;
import org.hibernate.criterion.Order;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.impl.CriteriaImpl.Subcriteria;

/**
 * Responsável por modificar um Criteria para adicionar a ordenação.
 * 
 * @author Marco
 */
public class OrderCriteriaService {

    /**
     * Modifica o criteria para ordenar a consulta.
     * 
     * @param criteria criteria que será modificado.
     * @param data objeto com os atributos da ordenação.
     */
    public void order(Criteria criteria, PagingData data) {
        if (data.getOrderBy().indexOf('.') == -1) {
            this.getCriteria(criteria).addOrder(data.isOrderAscending() ? Order.asc(data.getOrderBy()) : Order.desc(data.getOrderBy()));
        } else {
            this.orderAssociation(this.getCriteria(criteria), data);
        }
    }

    /**
     * Devolve a criteria mais alta na hierarquia.
     * 
     * @param criteria a criteira base.
     * @return a criteria mais alta na hierarquia.
     */
    private Criteria getCriteria(Criteria criteria) {
        if (criteria instanceof Subcriteria) {
            Criteria parent = ((Subcriteria) criteria).getParent();
            while (parent instanceof Subcriteria) {
                parent = ((Subcriteria) parent).getParent();
            }
            return parent;
        }
        return criteria;
    }

    /**
     * Modifica o criteria para ordenar a consulta. Apenas para quando a ordenação é feita por um atributo em uma associação.
     * 
     * @param criteria criteria que será modificado.
     * @param data objeto com os atributos da ordenação.
     */
    private void orderAssociation(Criteria criteria, PagingData data) {
        AssociationElement lastElement = null;
        StringTokenizer tokenizer = new StringTokenizer(data.getOrderBy(), ".");
        String token = tokenizer.nextToken();
        Map<String, AssociationElement> associationElements = new HashMap<String, AssociationElement>();
        while (tokenizer.hasMoreTokens()) {
            AssociationElement currentElement = new AssociationElement(token);
            associationElements.put(currentElement.getPath(), currentElement);
            if (!this.foundAlias(criteria, currentElement, lastElement)) {
                Criteria subcriteria =
                        criteria.createCriteria(currentElement.compose(lastElement), currentElement.getAlias(),
                        CriteriaSpecification.LEFT_JOIN);
                currentElement.setCriteria(subcriteria);
            }
            token = tokenizer.nextToken();
            lastElement = currentElement;
        }
        AssociationElement element = associationElements.get(lastElement.getPath());
        element.getCriteria().addOrder(data.isOrderAscending() ? Order.asc(token) : Order.desc(token));
    }

    /**
     * Busca por um alias no criteria.
     * 
     * @param criteria criteria sendo alterado.
     * @param currentElement elemento atual.
     * @param lastElement último elemento.
     * @return <code>true</code> caso encontrou alias, ou <code>false</code> caso contrário.
     */
    @SuppressWarnings("unchecked")
    private boolean foundAlias(Criteria criteria, AssociationElement currentElement, AssociationElement lastElement) {
        CriteriaImpl criteriaImpl = (CriteriaImpl) criteria;
        Iterator<Subcriteria> subcriteriaIterator = criteriaImpl.iterateSubcriteria();
        while (subcriteriaIterator.hasNext()) {
            Subcriteria subcriteria = subcriteriaIterator.next();
            if (subcriteria.getPath().equals(currentElement.getPath()) || subcriteria.getPath().equals(currentElement.compose(lastElement))) {
                currentElement.setAlias(subcriteria.getAlias() != null ? subcriteria.getAlias() : subcriteria.getPath());
                currentElement.setCriteria(subcriteria);
                return true;
            }
        }
        currentElement.setAlias(currentElement.getPath());
        return false;
    }

    /**
     * Elemento da associação que define a ordenação.
     */
    private static class AssociationElement {

        private String path;

        private String alias;

        private Criteria criteria;

        /**
         * Contrutor.
         * 
         * @param path o caminho do elemento de associação.
         */
        public AssociationElement(String path) {
            this.path = path;
        }

        /**
         * Devolve o alias.
         * 
         * @return o alias.
         */
        public String getAlias() {
            return this.alias;
        }

        /**
         * Define o alias.
         * 
         * @param alias o alias.
         */
        public void setAlias(String alias) {
            this.alias = alias;
        }

        /**
         * Devolve o caminho.
         * 
         * @return o caminho.
         */
        public String getPath() {
            return this.path;
        }

        /**
         * Devolve a criteria.
         * 
         * @return a criteria.
         */
        public Criteria getCriteria() {
            return this.criteria;
        }

        /**
         * Define a criteria.
         * 
         * @param criteria a criteria.
         */
        public void setCriteria(Criteria criteria) {
            this.criteria = criteria;
        }

        /**
         * Devolve o caminho composto pela concatenação do caminho (ou alias, quando existir) do elemento de associação passado como
         * parâmetro com o caminho deste elemento.
         * 
         * @param other o outro elemento.
         * @return o novo caminho composto.
         */
        public String compose(AssociationElement other) {
            return other == null ? this.path : (other.getAlias() != null ? other.getAlias() : other.getPath()) + '.' + this.path;
        }

        /**
         * Devolve a representação desse objeto.
         * 
         * @return a representação desse objeto.
         */
        @Override
        public String toString() {
            return this.path + " - " + this.alias;
        }
    }
}
